package me.ycy.android.couchbase.server.replication

import scala.reflect.{ BeanProperty, BooleanBeanProperty }
import org.codehaus.jackson.annotate._

import java.util.Date
import java.util.{ List => jList }
import scala.collection.JavaConverters._
import scala.collection.JavaConversions._
import scala.collection.immutable.TreeMap

import android.util.Log
import android.content.{ BroadcastReceiver, Context, Intent }
import android.net.ConnectivityManager
import android.widget.Toast

import org.ektorp._
import org.ektorp.changes._

object Replicator {
  class UserCtx {
    @BeanProperty var name: String = null

    var roles: List[String] = List("_admin")
    def getRoles: jList[String] = roles.asJava
    def setRoles(x: jList[String]) = { roles = x.toList }
  }
}

// NOTE: comment fields are deserialize only
class Replicator {
  import Replicator._

  var id: String = _
  @JsonProperty("_id") def getId = id
  @JsonProperty("_id") def setId(x: String) = { id = x }

  var revision: String = _
  @JsonProperty("_rev") def getRevision = revision
  @JsonProperty("_rev") def setRevision(x: String) = { revision = x }

  var replicationId: String = _
  //@JsonProperty("_replication_id")
  //def getReplicationId = replicationId
  @JsonProperty("_replication_id")
  def setReplicationId(x: String) = { replicationId = x }

  var replicationState: String = _
  //@JsonProperty("_replication_state")
  //def getReplicationState = replicationState
  @JsonProperty("_replication_state")
  def setReplicationState(x: String) = { replicationState = x }

  var replicationStateTime: Date = new Date()
  //@JsonProperty("_replication_state_time")
  //def getReplicationStateTime = replicationStateTime
  @JsonProperty("_replication_state_time")
  def setReplicationStateTime(x: Date) = { replicationStateTime = x }

  @BooleanBeanProperty var continuous: Boolean = false

  var createTarget: Boolean = false
  @JsonProperty("create_target")
  def getCreateTarget = createTarget
  @JsonProperty("create_target")
  def setCreateTarget(x: Boolean) = { createTarget = x }

  @BeanProperty var source: String = _
  @BeanProperty var target: String = _

  var userCtx: UserCtx = new UserCtx
  @JsonProperty("user_ctx") def getUserCtx = userCtx
  @JsonProperty("user_ctx") def setUserCtx(x: UserCtx) = { userCtx = x }

  // custom data
  var maxRetryInterval: Long = 1800
  @JsonProperty("max_retry_interval")
  def getMaxRetryInterval = maxRetryInterval
  @JsonProperty("max_retry_interval")
  def setMaxRetryInterval(x: Long) = { maxRetryInterval = x }

  // from /_active_tasks
  var startedOn: Date = new Date(0)
  var updatedOn: Date = new Date(0)
  var status: String = "-"
}

object ReplicatorDb extends Thread {
  import scala.collection.mutable.{ Map => mMap }
  val Tag = "CouchbaseServer"

  var db: CouchDbConnector = null

  // --------------- replicators ----------------------

  type RepT = Map[Long, Replicator]
  private var replicators: RepT = Map()
  private var repOrder: Array[Long] = Array()
  val replicatorsLock = new Object

  // use with cautious, will dead lock nest call.
  def modifyReplicators(func: RepT => RepT) = replicatorsLock.synchronized {
    replicators = func(replicators)
    repOrder = replicators.values.map(_.id).toList.sorted.
      map(_.hashCode.toLong).toArray
  }

  def withReplicators[T](func: RepT => T): T = replicatorsLock.synchronized {
    func(replicators)
  }

  def getReplicator(pos: Int) = {
    if (pos < repOrder.size) replicators(repOrder(pos))
    else null
  }

  // ----------------- ui ------------------------
  // set by main activity
  private var _updateUI: () => Unit = () => ()

  val updateUILock = new Object
  def setUiUpdater(f: () => Unit) = updateUILock.synchronized {
    _updateUI = f
  }

  def resetUiUpdater() = updateUILock.synchronized {
    _updateUI = () => ()
  }

  def updateUI(): Unit = updateUILock.synchronized {
    Log.d(Tag, "replication UI update.")
    _updateUI()
  }

  def waitDb() = while (db == null) { Thread.sleep(1000) }

  def catchTrivalError[T](tag: String,
                          id: String = null)(func: => Unit): Unit = {
    val doc = if (id != null) "document '" + id + "'"  else "document"
    try {
      func
    }
    catch {
      case _: DocumentNotFoundException => {
        Log.w(Tag, doc + " not found")
      }
      case _: DbAccessException => {
        Log.w(Tag, doc + " format or connection error, ignore")
      }
    }
  }

  // load & update read from couchdb, and update local
  def load() = {
    catchTrivalError(Tag) {
      val ids = db.getAllDocIds().toList.filterNot(_.startsWith("_design"))
      modifyReplicators { _ => {
        var r: RepT = new TreeMap()
        ids.foreach { id => {
          catchTrivalError(Tag) {
            val doc = db.get(classOf[Replicator], id)
            r += (id.hashCode.toLong -> doc)
          }
        }}
        r
      }}
    }
  }

  def update(id: String): Unit = {
    catchTrivalError(Tag, id) {
      val doc = db.get(classOf[Replicator], id)
      modifyReplicators { rep => {
        rep + (id.hashCode.toLong -> doc)
      }}
    }
  }

  // save & delete modify couchdb, not local. local updated automatically by
  // monitor couchdb changes.
  def save(r: Replicator) = {
    // first try to get corret rev
    var doc: Replicator = null
    catchTrivalError(Tag, r.id) {
      doc = db.get(classOf[Replicator], r.id)
    }

    withReplicators { rep => {
      if (doc == null) {
        r.revision = null
        db.create(r)
      }
      else {
        r.revision = doc.revision
        db.update(r)
      }
    }}
  }

  def delete(r: Replicator) = {
    catchTrivalError(Tag) {
      db.delete(r)
    }
  }

  def onDocumentChange(change: DocumentChange): Unit = {
    Log.d(Tag, "replicator db change: " + change)

    val id = change.getId
    val deleted = change.isDeleted()

    if (id.startsWith("_design")) {
      Log.i(Tag, "design doc " + id + " change, ignore")
      return
    }

    if (deleted) {
      modifyReplicators(_ - id.hashCode)
      updateUI()
    }
    else {
      update(id)
      updateUI()
    }
  }

  var running = true
  override def run = {
    Log.i(Tag, "replicator thread start.")

    // load data
    try {
      waitDb()
      load()
      updateUI()
    }
    catch {
      case e => {
        Log.e(Tag, "replicator load data error " + e.getMessage)
        e.printStackTrace
      }
    }

    while (running) {
      try {
        // wait db
        waitDb()

        // get feeds
        val cmd = new ChangesCommand.Builder().continuous(true).build()
        val feed: ChangesFeed = db.changesFeed(cmd)

        // monitor change
        while (true) {
          onDocumentChange(feed.next)
        }
      }
      catch {
        case _: DocumentNotFoundException => db.createDatabaseIfNotExists()
        // NOTE: ektorp will also interrupt thread when some error occur.
        case e: DbAccessException => { // connection error
          // clear db
          db = null
          // TODO, try to reconnect db
        }
        case _: InterruptedException => Log.i(Tag, "replicator thread interrupted")
        case e => {
          Log.e(Tag, "replicator thread error " + e.getMessage)
          e.printStackTrace
        }
      }
      try {
        if (running) Thread.sleep(5000) // wait for 5 sec
      }
      catch { case _ => }
    }

    Log.i(Tag, "replicator thread exit.")
  }

}



class ConnectionChangeReceiver extends BroadcastReceiver {
  val Tag = "CouchbaseServer"

  override def onReceive(ctx: Context, i: Intent): Unit = {
    val cm = ctx.getSystemService(Context.CONNECTIVITY_SERVICE).
      asInstanceOf[ConnectivityManager]

    /*
    Toast.makeText(ctx, "network connectivity changed, trigger replication.",
                   Toast.LENGTH_SHORT).show()
                   */
    Log.i(Tag, "network connectivity changed, trigger replication.")
    val values = ReplicatorDb.withReplicators(_.values)
    for (r <- values) {
      try {
        ReplicatorDb.save(r)
        Log.d(Tag, r.id + " triggered")
      }
      catch { case _ => }
    }
  }
}
